/**
 * @author ens12ilt - ens12ple
 */

package model.neuralnetwork;

import java.util.ArrayList;
import java.util.Collections;

import config.Config;

import log.Log;
import utils.ArrayUtils;

public class NeuralNetwork {

	private static final String LOG_TAG = NeuralNetwork.class.getSimpleName();

	private int _numberInputs;
	private int _numberOutputs;
	private int _numberHiddenLayers;
	private int _numberNeuronsPerHiddenLayer;
	private ArrayList<NeuronLayer> _layers;

	/** 
	 * 
	 * @param ni number of inputs
	 * @param no number of outputs
	 * @param nhl number of hidden layers
	 * @param nnphl number of neurons per hidden layer
	 */
	public NeuralNetwork(int ni, int no, int nhl, int nnphl){
		_numberInputs = ni;
		_numberOutputs = no;
		_numberHiddenLayers = nhl;
		_numberNeuronsPerHiddenLayer = nnphl;
		_layers = createNeuralNetwork();
	}
	
	/**
	 * 
	 * @param no number of outputs
	 * @param hl number of hidden layers
	 */
	public NeuralNetwork(int no, int[][] hl){
		_numberOutputs = no;
		_numberHiddenLayers = hl.length;
		_layers = createNeuralNetwork(hl);
	}

	public NeuronLayer getOutputLayer(){
		return _layers.get(_layers.size() - 1);
	}

	public NeuronLayer getFirstHiddenLayer(){
		return _layers.get(0);
	}
	
	private ArrayList<NeuronLayer> createNeuralNetwork(int[][] hl){
		ArrayList<NeuronLayer> l = new ArrayList<NeuronLayer>();
		for(int i = 0; i < hl.length; i++){
			l.add(new NeuronLayer(hl[i][0], hl[i][1]));
		}
		l.add(new NeuronLayer(_numberOutputs, hl[hl.length-1][0]));
		return l;
	}

	private ArrayList<NeuronLayer> createNeuralNetwork(){
		ArrayList<NeuronLayer> l = new ArrayList<NeuronLayer>();
		for(int i = 0; i < _numberHiddenLayers; i++){
			l.add(new NeuronLayer(_numberNeuronsPerHiddenLayer, _numberInputs));
		}
		l.add(new NeuronLayer(_numberOutputs, _numberNeuronsPerHiddenLayer));
		return l;
	}

	/**
	 * 
	 * @param inputData input values, here pixel values
	 * @return the index of guessed emotion
	 */
	public int guess(ArrayList<Double> inputData){
		ArrayList<Double> inputs = new ArrayList<Double>(inputData);
		ArrayList<Double> outputsTmp = new ArrayList<Double>();
		for(int i = 0; i < _layers.size(); i++){
			for(int j = 0; j < _layers.get(i).getLayerSize(); j++){
				Neuron n = _layers.get(i).getNeuronInLayer(j);
				n.setInputs(inputs);
				outputsTmp.add(n.activationFunction(n.calculateWeightedSum()));
			}
			inputs = new ArrayList<Double>(outputsTmp);
			outputsTmp.clear();
		}
		Log.i(LOG_TAG, "OUT = " + inputs);
		Double retD = Collections.max(inputs);
		int ret = inputs.indexOf(retD);
		return ret + 1;
	}

	/**
	 * 
	 * @param inputsData input values, pixel values of the figure
	 * @param expectedOutputValues the four expected values, 1 for the good emotion, 0 for others
	 * @return the error between what the network guesses and what is expected.
	 */
	public double learn(ArrayList<Double> inputsData, ArrayList<Double> expectedOutputValues){
		ArrayList<Double> inputs = new ArrayList<Double>(inputsData);
		NeuronLayer hiddenLayer;
		Neuron n;
		
		/* Trial to guess the emotion */
		for(int i = 0; i < _layers.size() - 1; i++){
			hiddenLayer = _layers.get(i);
			ArrayList<Double> out = new ArrayList<Double>();
			for(int j = 0; j < hiddenLayer.getLayerSize(); j++){
				n = hiddenLayer.getNeuronInLayer(j);
				n.setInputs(inputs);
				out.add(n.activationFunction(n.calculateWeightedSum()));
			}
			inputs.clear();
			inputs = out;
		}
		NeuronLayer outputLayer = getOutputLayer();
		ArrayList<Double> outputs = new ArrayList<Double>();
		for(int i = 0; i < outputLayer.getLayerSize(); i++){
			n = outputLayer.getNeuronInLayer(i);
			n.setInputs(inputs);
			outputs.add(n.activationFunction(n.calculateWeightedSum()));
		}

		if(Config.DEBUG){
			for(int i = 0; i < outputs.size(); i++){
				Log.i(LOG_TAG, "OUTPUTS " + i + " : " + outputs.get(i));
			}
		}

		/* Error calculation for output layer */
		ArrayList<Double> errorOnOutput = new ArrayList<Double>();
		double error = 0;
		for(int i = 0; i < getOutputLayer().getLayerSize(); i++){
			Neuron neuron = outputLayer.getNeuronInLayer(i);
			error = neuron.getLastOutput()*(1 - neuron.getLastOutput())*(expectedOutputValues.get(i) - neuron.getLastOutput());
			errorOnOutput.add(error);
		}
		
		double errorToRet = ArrayUtils.sum(errorOnOutput);
		if(Config.DEBUG){
			Log.i(LOG_TAG, "ERROR ON OUTPUTS = " + errorOnOutput);
		}
		
		/* Update weight in output layer */
		for(int i = 0; i < getOutputLayer().getLayerSize(); i++){
			Neuron neuron = outputLayer.getNeuronInLayer(i);
			neuron.updateWeight(ArrayUtils.sum(errorOnOutput));
		}

		/* Update weight in other layers */
		ArrayList<Double> errorOnLayer = new ArrayList<Double>();
		ArrayList<Double> previousErrors = new ArrayList<Double>(errorOnOutput);
		for(int i = _layers.size() - 2; i >= 0; i--){
			hiddenLayer = _layers.get(i);
			outputLayer = _layers.get(i + 1);
			/* Calculation of the error for each neuron in this layer */
			for(int h = 0; h < hiddenLayer.getLayerSize(); h++){
				double sumWeightedOut = 0;
				Neuron hNeuron = hiddenLayer.getNeuronInLayer(h);
				/* Calculation of the weighted sum */
				for(int j = 0; j < outputLayer.getLayerSize(); j++) {
					Neuron oNeuron = outputLayer.getNeuronInLayer(j);
					sumWeightedOut += oNeuron.getWeight(h) * previousErrors.get(j);
				}
				sumWeightedOut *= hNeuron.getLastOutput() * (1 - hNeuron.getLastOutput());
				errorOnLayer.add(sumWeightedOut);
				hNeuron.updateWeight(sumWeightedOut);
			}
			previousErrors = new ArrayList<Double>(errorOnLayer);
		}
		return errorToRet;
	}
}
